﻿// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;


namespace Microsoft.DotNet.Interactive.Http;

internal class HttpApiTunnelingRouter : IRouter
{
    private readonly HtmlNotebookFrontendEnvironment _frontendEnvironment;

    private readonly ConcurrentDictionary<Uri, string> _bootstrapperScripts = new();

    public HttpApiTunnelingRouter(HtmlNotebookFrontendEnvironment frontendEnvironment)
    {
        _frontendEnvironment = frontendEnvironment;
    }

    public VirtualPathData GetVirtualPath(VirtualPathContext context)
    {
        return null;
    }

    public async Task RouteAsync(RouteContext context)
    {
        if (context.HttpContext.Request.Method == HttpMethods.Post)
        {
            await HandlePostVerb(context);
        }

        if (context.HttpContext.Request.Method == HttpMethods.Get)
        {
            await HandleGetVerb(context);
        }
    }

    private Task HandleGetVerb(RouteContext context)
    {
        var segments =
            context.HttpContext
                .Request
                .Path
                .Value
                .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(s => s.ToLower())
                .ToArray();
        if (segments.FirstOrDefault() == "apitunnel")
        {
            context.Handler = async httpContext =>
            {
                httpContext.Response.ContentType = "text/javascript";
                var key = context.HttpContext.Request.GetUri();
                if (_bootstrapperScripts.TryGetValue(key, out var scriptCode))
                {
                    await httpContext.Response.WriteAsync(scriptCode);
                }
                else
                {
                    httpContext.Response.StatusCode = 404;
                }

                await httpContext.Response.CompleteAsync();

            };

        }

        return Task.CompletedTask;
    }

    private async Task HandlePostVerb(RouteContext context)
    {
        var segments =
            context.HttpContext
                .Request
                .Path
                .Value
                .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
        if (segments.FirstOrDefault() == "apitunnel")
        {
            using var reader = new StreamReader(context.HttpContext.Request.Body);
            var source = await reader.ReadToEndAsync();

            var requestBody = JsonDocument.Parse(source).RootElement;

            var apiUri = new Uri(requestBody.GetProperty("tunnelUri").GetString());
            var frontendType = requestBody.GetProperty("frontendType").GetString();
            var hash = $"{Guid.NewGuid():N}";
            var bootstrapperUri = new Uri(apiUri, $"apitunnel/{frontendType}/{hash}/bootstrapper.js");
            _frontendEnvironment.SetApiUri(apiUri);

            _bootstrapperScripts.GetOrAdd(bootstrapperUri, key => GenerateBootstrapperCode(apiUri, frontendType, hash));

            context.Handler = async httpContext =>
            {
                httpContext.Response.ContentType = "text/plain";
                var response = new 
                {
                    bootstrapperUri = bootstrapperUri.ToString() 
                };
                await httpContext.Response.WriteAsync(JsonSerializer.Serialize( response));
                await httpContext.Response.CompleteAsync();
            };
        }
    }

    private string GenerateBootstrapperCode(Uri externalUri, string frontendType, string hash)
    {
        string template = $@"
// ensure `require` is available globally
(function (global) {{
    if (!global) {{
        global = window;
    }}
    let bootstrapper_$FRONTENDTYPE$_$HASH$ = function () {{
        let loadDotnetInteractiveApi = function () {{
            // use probing to find host url and api resources
            // load interactive helpers and language services
            let dotnetInteractiveRequire = require.config({{
                context: '$HASH$',
                paths: {{
                    'dotnet-interactive': '$EXTERNALURI$resources'
                }},
                urlArgs: 'cacheBuster=$HASH$'
            }}) || require;

            global.dotnetInteractiveRequire = dotnetInteractiveRequire;

            global.configureRequireFromExtension = function (extensionName, extensionCacheBuster) {{
                let paths = {{}};
                paths[extensionName] = `$EXTERNALURI$extensions/${{extensionName}}/resources/`;

                let internalRequire = require.config({{
                    context: extensionCacheBuster,
                    paths: paths,
                    urlArgs: `cacheBuster=${{extensionCacheBuster}}`
                }}) || require;

                return internalRequire
            }};

            dotnetInteractiveRequire([
                'dotnet-interactive/dotnet-interactive'
            ],
                function (dotnet) {{
                    dotnet.init(global);
                    console.log('dotnet-interactive js api initialised');
                }},
                function (error) {{
                    console.log(error);
                }}
            );

            console.log('execution of  boostrapper function bootstrapper_$FRONTENDTYPE$_$HASH$ completed');
        }}
{JavascriptUtilities.GetCodeForEnsureRequireJs(new Uri("https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"), "loadDotnetInteractiveApi")}
    }};

    console.log('installed boostrapper function bootstrapper_$FRONTENDTYPE$_$HASH$');
    bootstrapper_$FRONTENDTYPE$_$HASH$();
}})(window);
";

        return template
            .Replace("$HASH$", hash)
            .Replace("$EXTERNALURI$", externalUri.AbsoluteUri)
            .Replace("$FRONTENDTYPE$", frontendType);
    }
}